import pickle
import time
import bisect
import difflib
import logging
import random
import os, shutil

class Item:
    def __init__(self, question, answer, tags=[], ato = 0, choices = [], forward = True):
        self.__question = question
        self.__answer   = answer
        self.__ato      = ato
        self.__forward  = forward   
        self.__choices  = choices
        self.__tags     = tags
        self.__history  = []

    def question(self):
        return self.__question

    def answer(self):
        return self.__answer

    def choices(self):
        return self.__choices

    def setChoices(self, choices):
        self.__choices = choices

    def setAto(self, ato):
        self.__history.append((self.__ato, int(time.time())))
        self.__ato = ato

    def getSchedule(self):

        _sceduleat = None

        if self.__history:
            _lastAto, _time = self.__history[-1]

            # Schedule after 10 mins
            if _lastAto <= 0:
                _sceduleat = _time + 600     # 10 mins
            elif _lastAto == 1:
                _sceduleat = _time + 3600    # 1  hrs
            elif _lastAto == 2:
                _sceduleat = _time + 14400   # 4  hrs
            elif _lastAto == 3:
                _sceduleat = _time + 86400   # 1  day
            elif _lastAto == 4:
                _sceduleat = _time + 345600  # 4  days
            else:
                _sceduleat = _time + 1036800 # 12 days

        # If no history, schedule it for now
        else:
            _sceduleat = time.time()

        return _sceduleat

    def forward(self):
        return self.__forward

    def tags(self):
        return self.__tags

    def __str__(self):
        return "%-50s| %-50s| %-10s| %-100s| %d" % \
               (self.__question,
                self.__answer,
                self.__tags,
                self.__choices,
                self.__ato)

class DB:
    END_OF_WORLD = 10

    @staticmethod
    def load(fname):
        _db = None
        try:
            logging.debug("Loading from %s", fname)
            if fname:
                _file = open(fname, 'rb')
                _db = pickle.load(_file)
                _file.close()

        except Exception, e:
            if fname:
                logging.exception('Couldnt load %s', fname)

        _db = _db if _db else DB()
        logging.debug("Loaded %d items", _db.numItems())
        _db.sort()

        return _db

    def __init__(self):
        self.__items = []
        self.__index = 0
        self.__timer = 0

    def shuffle(self):
        logging.info("Shuffling")
        random.shuffle(self.__items)

    def numItems(self):
        return len(self.__items)

    def sort(self):
        logging.debug("Sorting on schedule")
        self.__items.sort(key=lambda x: x.getSchedule())

    def getNextItem(self):
        _index = self.__index

        if self.__index > DB.END_OF_WORLD:
            self.sort()
            _index = 0

        # FIXME: Cache the length
        _len = len(self.__items)
        if _index >= _len and _len > 0:
            _index = 0

        elif _len == 0:
            return None

        _item = self.__items[_index]
        self.__index = _index + 1

        self.__timer = time.time()

        return _item

    # heuristically assign the memory
    def done(self, success=True, took_hint=False):
        _time = int(time.time() - self.__timer)
        self.__timer = 0

        _ato = 0

        # if right answer in 5/15 secs, w/o hint
        # score is 5/4 respectively
        if   success and _time <= 4 and not took_hint:
            _ato = 5
        elif success and _time <= 8 and not took_hint:
            _ato = 4
        # If took longer than 15 secs to give w or w/o
        # correct answer, 3 or 2
        elif success and not took_hint:
            _ato = 3
        elif success:
            _ato = 2
        # If she gave wrong answer, assume
        # that the person is confused
        elif success is not None:
            _ato = 1
        # Totally gave up, dont know at all
        else:
            _ato = 0

        assert self.__index > 0
        _item = self.__items[self.__index - 1]

        logging.debug("Setting ato of %s to %d [time=%d took_hint=%s]", _item.answer(), _ato, _time, took_hint)

        _item.setAto(_ato)

    def addItem(self, item):
        self.__items.append(item)

    def generateChoices(self):

        # collect possible options
        _fdict = []
        _rdict = []
        for _item in self.__items:
            if _item.forward():
                _fdict.append(_item.answer())
                _fdict.extend(_item.choices())
                _rdict.append(_item.question())
            else:
                _rdict.append(_item.answer())
                _fdict.append(_item.question())

        _len_dict  = len(_rdict)
        if _len_dict < 20:
            return

        # Remove duplicates
        _fdict = list(set(_fdict))
        _rdict = list(set(_rdict))

        num_choices = 4

        # Choose algo
        _algo = self.__findSimiliarOnDiff

        for _item in self.__items:
            _item_num_choices = len(_item.choices())
            if _item_num_choices < num_choices:
                _answer = _item.answer().strip().lower()
                _item.setChoices(_item.choices() + \
                    filter(lambda x: x.strip().lower() != _answer,
                        _algo(_answer,
                            _fdict if _item.forward() else _rdict,
                            num_choices - _item_num_choices)))

        
    def __findSimiliarOnLength(self, answer, possibilities, n):
        possibilities.sort(cmp=lambda x, y: len(x) - len(y))

        # threshold for the sizes
        threshold = 1

        # calc sizes
        _lens_dict = map(lambda i: len(i) / threshold, possibilities)

        _len_dict  = len(possibilities)

        _find_choices_mid = n/2
        _len_answer = len(answer) / threshold
        _possible_choice_end = bisect.bisect(_lens_dict, _len_answer)

        if _len_dict - _possible_choice_end < _find_choices_mid:
            _possible_choice_end = _len_dict - _find_choices_mid
        elif _possible_choice_end < _find_choices_mid:
            _possible_choice_end = _find_choices_mid

        return possibilities[_possible_choice_end - _find_choices_mid : \
                _possible_choice_end + _find_choices_mid]

    def __findSimiliarOnDiff(self, answer, possibilities, n):
        return difflib.get_close_matches(answer, possibilities, n, cutoff=0.2)

    def __getstate__(self):
        odict = self.__dict__.copy() # copy the dict since we change it
        del odict['_DB__index']      # remove index entry
        del odict['_DB__timer']      # remove timer entry
        return odict

    def __setstate__(self, dict):
        self.__dict__.update(dict)
        self.__index = 0
        self.__timer = 0

    def __str__(self):
        return '\n'.join(map(lambda x: str(x), self.__items))

    def loadQnAEveryLine(self, fname, addReverse = True, tags=[]):
        logging.debug("Trying to load from %s", fname)
        _file = open(fname, 'rb')
        _items = 0
        while True:
            line = _file.readline().strip()
            if not line:
                break
            question = line
            answer = _file.readline().strip()
            
            _item = Item(question, answer, tags=tags)
            self.addItem(_item)
            logging.debug("Added %s", _item)

            _items += 1
            if addReverse:
                self.addItem(Item(answer, question, forward=False, tags=tags))
                logging.debug("Added reverse")
                _items += 1


        _file.close()
        logging.info("Added %d items", _items)
        return _items

    @staticmethod
    def store(db, fname):
        logging.info("Saving to %s", fname)
        
        _backups = 3
        for i in range(_backups, 0, -1):
            _fname = '%s.%d' % (fname, i)
            if os.path.exists(_fname):
                logging.debug("Backing up %s", _fname)
                shutil.move(_fname, '%s.%d' % (fname, i + 1))

        if os.path.exists(fname):
            logging.debug("Backing up %s", fname)
            shutil.move(fname, '%s.1' % fname)

        _file = open(fname, 'wb')
        pickle.dump(db, _file)
        _file.close()

if __name__ == '__main__':
    db = DB()
#    print dir(db)
#    pkl = pickle.dumps(db)
#    dbl = pickle.loads(pkl)
#    print dir(dbl)

    # db.addItem(Item('What',          'Yenu'))
    # db.addItem(Item('Why',           'Yake'))
    # db.addItem(Item('Where',        'Yelli'))
    # db.addItem(Item('This',           'Idu'))
    # db.addItem(Item('That',           'Adu'))
    # db.addItem(Item('Dont know', 'Gottilla'))
    # db.addItem(Item('Know',         'Gottu'))
    # db.addItem(Item('Chair',       'Kurchi'))
    # db.addItem(Item('Kitiki',      'Window'))

    db.loadQnAEveryLine('simple.txt', tags=['simple'])
    db.generateChoices()

    print db
